#!/usr/bin/env python
# line-directive.py - Transform line numbers in error messages -*- python -*-
#
# This source file is part of the Swift.org open source project
#
# Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors
# Licensed under Apache License v2.0 with Runtime Library Exception
#
# See http://swift.org/LICENSE.txt for license information
# See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
#
# ----------------------------------------------------------------------------
#
# Converts line numbers in error messages according to "line directive"
# comments.
#
# ----------------------------------------------------------------------------

import bisect
import re
import subprocess
import sys

line_pattern = re.compile(
    r'^// ###sourceLocation\(file:\s*"([^"]+)",\s*line:\s*([0-9]+)\s*\)')


def _make_line_map(filename, stream=None):
    """
    >>> from StringIO import StringIO
    >>> _make_line_map('box',
    ... StringIO('''// ###sourceLocation(file: "foo.bar", line: 3)
    ... line 2
    ... line 3
    ... line 4
    ... // ###sourceLocation(file: "baz.txt", line: 20)
    ... line 6
    ... line 7
    ... '''))
    [(0, 'box', 1), (1, 'foo.bar', 3), (5, 'baz.txt', 20)]
    """
    result = [(0, filename, 1)]
    input = stream or open(filename)
    for i, l in enumerate(input.readlines()):
        m = line_pattern.match(l)
        if m:
            result.append((i + 1, m.group(1), int(m.group(2))))
    return result

_line_maps = {}


def fline_map(filename):
    map = _line_maps.get(filename)
    if map is None:
        map = _make_line_map(filename)
        _line_maps[filename] = map
    return map


def map_line(filename, line_num):
    assert(line_num > 0)
    map = fline_map(filename)
    index = bisect.bisect_left(map, (line_num, '', 0))
    base = map[index - 1]
    return base[1], base[2] + (line_num - base[0] - 1)


def run():
    if len(sys.argv) <= 1:
        import doctest
        doctest.testmod()
    else:
        dashes = sys.argv.index('--')
        sources = sys.argv[1:dashes]

        command = subprocess.Popen(
            sys.argv[dashes + 1:],
            stderr=subprocess.STDOUT,
            stdout=subprocess.PIPE,
            universal_newlines=True
        )

        error_pattern = re.compile(
            '^(' + '|'.join(re.escape(s) for s in sources) +
            '):([0-9]+):([0-9]+):(.*)')

        assertion_pattern = re.compile(
            '^(.*)(: file| at|#[0-9]+:) (' +
            '|'.join(re.escape(s) for s in sources) +
            ')(, line |:)([0-9]+)(.*)')

        while True:
            line = command.stdout.readline()
            if line == '':
                break
            l = line.rstrip('\n')
            m = error_pattern.match(l)
            if m:
                file, line_num = map_line(m.group(1), int(m.group(2)))
                line = '%s:%s:%s:%s\n' % (
                    file, line_num, int(m.group(3)), m.group(4))
            else:
                m = assertion_pattern.match(l)
                if m:
                    file, line_num = map_line(m.group(3), int(m.group(5)))
                    line = '%s%s %s%s%s%s\n' % (
                        m.group(1), m.group(2), file, m.group(4), line_num,
                        m.group(6))
            sys.stdout.write(line)

        sys.exit(command.wait())

if __name__ == '__main__':
    run()
